Skip to main content

Python OOP — Practical Notes

1. __init__() and Object Initialization

__init__() is called when a new object is created.

class User:
def __init__(self, user_id, name, email, age=None):
self.user_id = user_id
self.name = name
self.email = email
self.age = age

Creating an object:

user = User("user1", "Harsh", "harsh@gmail.com")

Conceptually:

User(...)

__init__(self, user_id, name, email, age=None)

self.user_id = "user1"
self.name = "Harsh"
self.email = "harsh@gmail.com"
self.age = None

Mandatory vs Optional Parameters

def __init__(self, user_id, surname=None):
  • user_id → mandatory

  • surname → optional

  • If surname isn't supplied → None

user1 = User("user1", "Verma")
user2 = User("user2")

Result:

user1.surname → "Verma"
user2.surname → None

Important terminology

Prefer:

__init__() initializes an object using the values passed when the object is created.

Rather than:

__init__() contains variables which should be passed.

The things inside the method definition are parameters.

The resulting:

self.name
self.age
self.email

are instance attributes.


2. self

self represents the current object/instance.

class User:
def __init__(self, name):
self.name = name

def display(self):
print(self.name)

When:

user = User("Harsh")
user.display()

is called, Python conceptually does:

display(user)

So:

self → the current object

Common mistake

Inside an instance method:

def validate_user(self):
if age < 13:
...

age is not automatically known as the object's age.

Use:

if self.age < 13:

Similarly:

age == None

should be:

self.age is None

Mental rule

If you mean this object's attribute, use self.attribute.

Examples:

self.name
self.age
self.email
self.purchase_count

3. None Checks

You may write:

age != None

but the preferred Python style is:

age is not None

and:

age is None

Example:

if self.age is not None and self.age < 13:
return False

4. Optional Attributes

A common design is:

class Dog:
def __init__(self, name, age, color=None):
self.name = name
self.age = age
self.color = color

Now:

buddy = Dog("Buddy", 9)
miles = Dog("Miles", 4, "brown")

Results:

buddy
├── name = "Buddy"
├── age = 9
└── color = None

miles
├── name = "Miles"
├── age = 4
└── color = "brown"

Both objects have a color attribute, but one has the value None.

This is usually preferable when color is conceptually a valid attribute but is optional.


5. Conditional Expressions

Instead of:

if self.color:
return f"{self.name} is {self.age} years old and is {self.color}."
return f"{self.name} is {self.age} years old."

Python allows a one-line conditional expression:

return (
f"{self.name} is {self.age} years old and is {self.color}."
if self.color
else f"{self.name} is {self.age} years old."
)

General syntax:

value_if_true if condition else value_if_false

However, don't sacrifice readability just to make something one line.


6. Class Variables vs Instance Variables

Instance variable

Belongs to an individual object:

self.name
self.age
self.color

Different objects can have different values.

Class variable

Belongs to the class:

class Dog:
species = "Canis familiaris"

All objects can access it:

Dog.species
buddy.species
miles.species

All produce:

Canis familiaris

Mental model:

Dog

species = "Canis familiaris"

┌───────┴───────┐
↓ ↓
buddy miles
│ │
name=Buddy name=Miles
age=9 age=4
color=None color=brown

7. Inheritance

Python does not use Java's extends keyword.

Java:

class Cat extends Animal

Python:

class Cat(Animal):

The class inside parentheses is the parent/base class.

class Animal:
def speak(self):
return "Some generic sound"


class Cat(Animal):
def speak(self):
return "Meow!"

Structure:

Animal

├── Cat
└── Dog

8. Inheriting Everything

If the child doesn't need any customization:

class Animal:
def __init__(self, name):
self.name = name

def speak(self):
return "Some sound"


class Dog(Animal):
pass

Then:

dog = Dog("Buddy")

The inherited Animal.__init__() is used automatically.

The dog gets:

dog.name
dog.speak()

Three common situations

1. Inherit unchanged
class Dog(Animal):
pass

2. Inherit + add methods
class Dog(Animal):
def bark(self):
...

3. Customize __init__
class Dog(Animal):
def __init__(...):
super().__init__(...)
...

9. super()

Suppose:

class Animal:
def __init__(self, name):
self.name = name

and:

class Cat(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed

super().__init__(name) means:

Run the parent's __init__().

So:

super().__init__(name)

runs:

Animal.__init__(name)

which creates:

self.name = name

Then Cat adds:

self.breed = breed

Mental model:

Cat initialization

├── super().__init__()
│ ↓
│ Animal initialization
│ ↓
│ self.name

└── Cat initialization

self.breed

10. You Do NOT Pass self to super()

A common mistake:

Super().__init__(self, user_id, name, age, email)

Correct:

super().__init__(user_id, name, email, age)

Why?

The parent's __init__() is already being called through super().

Python handles the object automatically.

So:

super().__init__(user_id, name, email, age)

maps to:

parent's user_id ← user_id
parent's name ← name
parent's email ← email
parent's age ← age

Do not manually pass self.


11. Child Class With Parent + Additional Attributes

Parent:

class User:
def __init__(self, user_id, name, email, age=None):
self.user_id = user_id
self.name = name
self.email = email
self.age = age

Child:

class Customer(User):
def __init__(self, user_id, name, email, age, purchase_count):
super().__init__(user_id, name, email, age)
self.purchase_count = purchase_count

The child doesn't recreate:

self.user_id = user_id
self.name = name
self.email = email
self.age = age

The parent does that.

The child only adds:

self.purchase_count = purchase_count

General pattern

class Child(Parent):
def __init__(self, parent_data, child_data):
super().__init__(parent_data)
self.child_data = child_data

12. Method Overriding

A child can provide its own implementation of a parent method.

class Animal:
def speak(self):
return "Some generic sound"


class Cat(Animal):
def speak(self):
return "Meow!"


class Dog(Animal):
def speak(self):
return "Woof!"

Now:

cat = Cat()
dog = Dog()

cat.speak()
# Meow!

dog.speak()
# Woof!

This is method overriding.

The child replaces the inherited implementation for that method.


13. super() vs Method Overriding

super() doesn't mean "inherit everything."

It means:

Access the parent implementation from the child.

For example:

class Cat(Animal):
def speak(self):
return super().speak() + " — Meow!"

This can be useful when you want to extend the parent's behavior rather than completely replace it.


14. Class Methods and cls

A class method is declared using:

@classmethod

Example:

class User:
count = 0

@classmethod
def get_count(cls):
return cls.count

cls represents the class itself.

Conceptually:

User.get_count()

becomes approximately:

get_count(User)

So:

cls → User

15. cls Is NOT a Keyword

Just like self, cls is a convention, not a Python keyword.

This technically works:

@classmethod
def get_count(x):
return x.count

But Python programmers conventionally use:

@classmethod
def get_count(cls):

because it immediately communicates:

This is a class method and this parameter represents the class.

Similarly:

def display(self):

uses self by convention.

Neither self nor cls is a reserved Python keyword.


16. Why Does cls Become the Class?

The important part is:

@classmethod

The decorator tells Python to bind the method to the class.

Therefore:

class User:
@classmethod
def get_count(cls):
...

and:

User.get_count()

conceptually becomes:

get_count(User)

The first parameter receives the class automatically.


17. Additional Parameters in a Class Method

If:

class User:
@classmethod
def get_count(cls, name):
print(cls)
print(name)

then:

User.get_count("Harsh")

maps conceptually to:

get_count(User, "Harsh")

Therefore:

cls → User
name → "Harsh"

The general pattern is:

Class.method(user_supplied_argument)

becomes approximately:

method(Class, user_supplied_argument)

18. self vs cls

MethodDecoratorAutomatically receivesConvention
Instance methodNoneObjectself
Class method@classmethodClasscls
Static method@staticmethodNothingNo self / cls

Instance method

class User:
def hello(self, name):
...
user.hello("Harsh")

Conceptually:

hello(user, "Harsh")

Therefore:

self → user
name → "Harsh"

Class method

class User:
@classmethod
def hello(cls, name):
...
User.hello("Harsh")

Conceptually:

hello(User, "Harsh")

Therefore:

cls → User
name → "Harsh"

19. User Count — Class Variable + Class Method

Requirement:

Keep track of how many users have been created.

Use a class variable:

class User:
user_count = 0

def __init__(self, user_id, name):
self.user_id = user_id
self.name = name

User.user_count += 1

Every time an object is created:

User object created

User.user_count += 1

Example:

user1 = User(1, "Harsh")
user2 = User(2, "Sarwvidya")
user3 = User(3, "John")

print(User.user_count)

Output:

3

20. Counting Child Objects

Because Customer and Admin inherit from User, their constructors can call:

super().__init__(...)

That causes the parent constructor to execute.

Therefore:

class Customer(User):
def __init__(self, ...):
super().__init__(...)

also increments:

User.user_count

So:

user1 = User(...)
customer1 = Customer(...)
admin1 = Admin(...)

can produce:

User.user_count = 3

21. Complete OOP Example

class User:
user_count = 0

def __init__(self, user_id, name, email, age=None):
self.user_id = user_id
self.name = name
self.email = email
self.age = age

User.user_count += 1

def validate_user(self):
if '@' not in self.email:
return False

if self.age is not None and self.age < 13:
return False

return True

def display_user(self):
if self.age is None:
print(
'user name is', self.name,
'user email is', self.email
)
else:
print(
'user name is', self.name,
'user email is', self.email,
'user age is', self.age
)

def update_email(self, updated_email):
self.email = updated_email

def adult_check(self):
if self.age is not None and self.age >= 18:
return True

return False

def describe(self):
print('hi')

@classmethod
def get_user_count(cls):
return cls.user_count


class Customer(User):

def __init__(
self,
user_id,
name,
email,
age,
purchase_count
):
super().__init__(user_id, name, email, age)
self.purchase_count = purchase_count

def describe(self):
print('hello')


class Admin(User):

def __init__(
self,
user_id,
name,
email,
age,
permissions
):
super().__init__(user_id, name, email, age)
self.permissions = permissions

def describe(self):
print('meow')

Objects:

user1 = Customer(
1,
'Harsh',
'harsh@gmail.com',
18,
200
)

admin1 = Admin(
2,
'Sarwvidya',
'sarwvidya@gmail.com',
18,
'can read'
)

Count:

print(User.get_user_count())

Output:

2

22. The Most Important Mental Models

Instance

self

this particular object

Class

cls

the class itself

Constructor

__init__()

initialize object

Inheritance

class Child(Parent)

Child inherits Parent

Parent initialization

super().__init__(...)

run Parent's __init__

Class method

@classmethod

first parameter automatically receives the class

Instance method

object.method(...)

first parameter automatically receives the object

23. Mistakes to Watch For

❌ Forgetting self

if age < 13:

if self.age < 13:

❌ Wrong attribute name

self.user

when the attribute was actually:

self.name

❌ Wrong capitalization

Super()

super()

Python is case-sensitive.


❌ Passing self to super()

super().__init__(self, user_id, name)

super().__init__(user_id, name)

❌ Thinking self is a keyword

It isn't.

❌ Thinking cls is a keyword

It isn't.

Both are conventional parameter names.

self → instance
cls → class

❌ Using Java inheritance syntax

class Dog extends Animal:

class Dog(Animal):

❌ Reimplementing the parent constructor unnecessarily

Instead of:

class Customer(User):
def __init__(self, user_id, name, email, age, purchase_count):
self.user_id = user_id
self.name = name
self.email = email
self.age = age
self.purchase_count = purchase_count

Prefer:

class Customer(User):
def __init__(self, user_id, name, email, age, purchase_count):
super().__init__(user_id, name, email, age)
self.purchase_count = purchase_count

The parent owns initialization of the parent attributes.


24. Production-Oriented Rule of Thumb

Think of inheritance as:

Parent

│ owns common behavior/data

Child

└── adds or specializes its own behavior/data

For example:

User
├── user_id
├── name
├── email
└── age

├── Customer
│ └── purchase_count

└── Admin
└── permissions

The child should generally reuse the parent's initialization and behavior rather than copy it.

Use:

super()

to reuse the parent's implementation when overriding something.